/* -*- Mode: java; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*-
 *
 * ***** BEGIN LICENSE BLOCK *****
 * Version: MPL 1.1/GPL 2.0
 *
 * The contents of this file are subject to the Mozilla Public License Version
 * 1.1 (the "License"); you may not use this file except in compliance with
 * the License. You may obtain a copy of the License at
 * http://www.mozilla.org/MPL/
 *
 * Software distributed under the License is distributed on an "AS IS" basis,
 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
 * for the specific language governing rights and limitations under the
 * License.
 *
 * The Original Code is Rhino code, released
 * May 6, 1999.
 *
 * The Initial Developer of the Original Code is
 * Netscape Communications Corporation.
 * Portions created by the Initial Developer are Copyright (C) 1997-1999
 * the Initial Developer. All Rights Reserved.
 *
 * Contributor(s):
 *   Igor Bukanov, igor@fastmail.fm
 *
 * Alternatively, the contents of this file may be used under the terms of
 * the GNU General Public License Version 2 or later (the "GPL"), in which
 * case the provisions of the GPL are applicable instead of those above. If
 * you wish to allow use of your version of this file only under the terms of
 * the GPL and not to allow others to use your version of this file under the
 * MPL, indicate your decision by deleting the provisions above and replacing
 * them with the notice and other provisions required by the GPL. If you do
 * not delete the provisions above, a recipient may use your version of this
 * file under either the MPL or the GPL.
 *
 * ***** END LICENSE BLOCK ***** */

package org.mozilla.javascript;

import java.io.IOException;
import java.io.InputStream;
import java.io.Reader;
import java.lang.reflect.Method;
import java.util.Map;

/**
 * Collection of utilities
 */

public class Kit {
  private final static class ComplexKey {
    private final Object key1;
    private final Object key2;
    private int hash;

    ComplexKey(Object key1, Object key2) {
      this.key1 = key1;
      this.key2 = key2;
    }

    @Override
    public boolean equals(Object anotherObj) {
      if (!(anotherObj instanceof ComplexKey))
        return false;
      ComplexKey another = (ComplexKey) anotherObj;
      return key1.equals(another.key1) && key2.equals(another.key2);
    }

    @Override
    public int hashCode() {
      if (hash == 0)
        hash = key1.hashCode() ^ key2.hashCode();
      return hash;
    }
  }

  /**
   * Reflection of Throwable.initCause(Throwable) from JDK 1.4 or nul if it is
   * not available.
   */
  private static Method Throwable_initCause = null;

  static {
    // Are we running on a JDK 1.4 or later system?
    try {
      Class<?> ThrowableClass = Kit.classOrNull("java.lang.Throwable");
      Class<?>[] signature = {ThrowableClass};
      Throwable_initCause = ThrowableClass.getMethod("initCause", signature);
    } catch (Exception ex) {
      // Assume any exceptions means the method does not exist.
    }
  }

  /**
   * Add <i>listener</i> to <i>bag</i> of listeners. The function does not
   * modify <i>bag</i> and return a new collection containing <i>listener</i>
   * and all listeners from <i>bag</i>. Bag without listeners always represented
   * as the null value.
   * <p>
   * Usage example:
   * 
   * <pre>
   *     private volatile Object changeListeners;
   * 
   *     public void addMyListener(PropertyChangeListener l)
   *     {
   *         synchronized (this) {
   *             changeListeners = Kit.addListener(changeListeners, l);
   *         }
   *     }
   * 
   *     public void removeTextListener(PropertyChangeListener l)
   *     {
   *         synchronized (this) {
   *             changeListeners = Kit.removeListener(changeListeners, l);
   *         }
   *     }
   * 
   *     public void fireChangeEvent(Object oldValue, Object newValue)
   *     {
   *     // Get immune local copy
   *         Object listeners = changeListeners;
   *         if (listeners != null) {
   *             PropertyChangeEvent e = new PropertyChangeEvent(
   *                 this, &quot;someProperty&quot; oldValue, newValue);
   *             for (int i = 0; ; ++i) {
   *                 Object l = Kit.getListener(listeners, i);
   *                 if (l == null)
   *                     break;
   *                 ((PropertyChangeListener)l).propertyChange(e);
   *             }
   *         }
   *     }
   * </pre>
   * 
   * @param listener
   *          Listener to add to <i>bag</i>
   * @param bag
   *          Current collection of listeners.
   * @return A new bag containing all listeners from <i>bag</i> and
   *         <i>listener</i>.
   * @see #removeListener(Object bag, Object listener)
   * @see #getListener(Object bag, int index)
   */
  public static Object addListener(Object bag, Object listener) {
    if (listener == null)
      throw new IllegalArgumentException();
    if (listener instanceof Object[])
      throw new IllegalArgumentException();

    if (bag == null)
      bag = listener;
    else if (!(bag instanceof Object[]))
      bag = new Object[]{bag, listener};
    else {
      Object[] array = (Object[]) bag;
      int L = array.length;
      // bag has at least 2 elements if it is array
      if (L < 2)
        throw new IllegalArgumentException();
      Object[] tmp = new Object[L + 1];
      System.arraycopy(array, 0, tmp, 0, L);
      tmp[L] = listener;
      bag = tmp;
    }

    return bag;
  }

  /**
   * Attempt to load the class of the given name. Note that the type parameter
   * isn't checked.
   */
  public static Class<?> classOrNull(ClassLoader loader, String className) {
    try {
      return loader.loadClass(className);
    } catch (ClassNotFoundException ex) {
    } catch (SecurityException ex) {
    } catch (LinkageError ex) {
    } catch (IllegalArgumentException e) {
      // Can be thrown if name has characters that a class name
      // can not contain
    }
    return null;
  }

  public static Class<?> classOrNull(String className) {
    try {
      return Class.forName(className);
    } catch (ClassNotFoundException ex) {
    } catch (SecurityException ex) {
    } catch (LinkageError ex) {
    } catch (IllegalArgumentException e) {
      // Can be thrown if name has characters that a class name
      // can not contain
    }
    return null;
  }

  /**
   * Throws RuntimeException to indicate failed assertion. The function never
   * returns and its return type is RuntimeException only to be able to write
   * <tt>throw Kit.codeBug()</tt> if plain <tt>Kit.codeBug()</tt> triggers
   * unreachable code error.
   */
  public static RuntimeException codeBug() throws RuntimeException {
    RuntimeException ex = new IllegalStateException("FAILED ASSERTION");
    // Print stack trace ASAP
    ex.printStackTrace(System.err);
    throw ex;
  }

  /**
   * Get listener at <i>index</i> position in <i>bag</i> or null if <i>index</i>
   * equals to number of listeners in <i>bag</i>.
   * <p>
   * For usage example, see {@link #addListener(Object bag, Object listener)}.
   * 
   * @param bag
   *          Current collection of listeners.
   * @param index
   *          Index of the listener to access.
   * @return Listener at the given index or null.
   * @see #addListener(Object bag, Object listener)
   * @see #removeListener(Object bag, Object listener)
   */
  public static Object getListener(Object bag, int index) {
    if (index == 0) {
      if (bag == null)
        return null;
      if (!(bag instanceof Object[]))
        return bag;
      Object[] array = (Object[]) bag;
      // bag has at least 2 elements if it is array
      if (array.length < 2)
        throw new IllegalArgumentException();
      return array[0];
    } else if (index == 1) {
      if (!(bag instanceof Object[])) {
        if (bag == null)
          throw new IllegalArgumentException();
        return null;
      }
      Object[] array = (Object[]) bag;
      // the array access will check for index on its own
      return array[1];
    } else {
      // bag has to array
      Object[] array = (Object[]) bag;
      int L = array.length;
      if (L < 2)
        throw new IllegalArgumentException();
      if (index == L)
        return null;
      return array[index];
    }
  }

  /**
   * If initCause methods exists in Throwable, call <tt>ex.initCause(cause)</tt>
   * or otherwise do nothing.
   * 
   * @return The <tt>ex</tt> argument.
   */
  public static RuntimeException initCause(RuntimeException ex, Throwable cause) {
    if (Throwable_initCause != null) {
      Object[] args = {cause};
      try {
        Throwable_initCause.invoke(ex, args);
      } catch (Exception e) {
        // Ignore any exceptions
      }
    }
    return ex;
  }

  static Object initHash(Map<Object, Object> h, Object key, Object initialValue) {
    synchronized (h) {
      Object current = h.get(key);
      if (current == null)
        h.put(key, initialValue);
      else
        initialValue = current;
    }
    return initialValue;
  }

  public static Object makeHashKeyFromPair(Object key1, Object key2) {
    if (key1 == null)
      throw new IllegalArgumentException();
    if (key2 == null)
      throw new IllegalArgumentException();
    return new ComplexKey(key1, key2);
  }

  static Object newInstanceOrNull(Class<?> cl) {
    try {
      return cl.newInstance();
    } catch (SecurityException x) {
    } catch (LinkageError ex) {
    } catch (InstantiationException x) {
    } catch (IllegalAccessException x) {
    }
    return null;
  }

  public static String readReader(Reader r) throws IOException {
    char[] buffer = new char[512];
    int cursor = 0;
    for (;;) {
      int n = r.read(buffer, cursor, buffer.length - cursor);
      if (n < 0)
        break;
      cursor += n;
      if (cursor == buffer.length) {
        char[] tmp = new char[buffer.length * 2];
        System.arraycopy(buffer, 0, tmp, 0, cursor);
        buffer = tmp;
      }
    }
    return new String(buffer, 0, cursor);
  }

  public static byte[] readStream(InputStream is, int initialBufferCapacity)
      throws IOException {
    if (initialBufferCapacity <= 0)
      throw new IllegalArgumentException("Bad initialBufferCapacity: "
          + initialBufferCapacity);
    byte[] buffer = new byte[initialBufferCapacity];
    int cursor = 0;
    for (;;) {
      int n = is.read(buffer, cursor, buffer.length - cursor);
      if (n < 0)
        break;
      cursor += n;
      if (cursor == buffer.length) {
        byte[] tmp = new byte[buffer.length * 2];
        System.arraycopy(buffer, 0, tmp, 0, cursor);
        buffer = tmp;
      }
    }
    if (cursor != buffer.length) {
      byte[] tmp = new byte[cursor];
      System.arraycopy(buffer, 0, tmp, 0, cursor);
      buffer = tmp;
    }
    return buffer;
  }

  /**
   * Remove <i>listener</i> from <i>bag</i> of listeners. The function does not
   * modify <i>bag</i> and return a new collection containing all listeners from
   * <i>bag</i> except <i>listener</i>. If <i>bag</i> does not contain
   * <i>listener</i>, the function returns <i>bag</i>.
   * <p>
   * For usage example, see {@link #addListener(Object bag, Object listener)}.
   * 
   * @param listener
   *          Listener to remove from <i>bag</i>
   * @param bag
   *          Current collection of listeners.
   * @return A new bag containing all listeners from <i>bag</i> except
   *         <i>listener</i>.
   * @see #addListener(Object bag, Object listener)
   * @see #getListener(Object bag, int index)
   */
  public static Object removeListener(Object bag, Object listener) {
    if (listener == null)
      throw new IllegalArgumentException();
    if (listener instanceof Object[])
      throw new IllegalArgumentException();

    if (bag == listener)
      bag = null;
    else if (bag instanceof Object[]) {
      Object[] array = (Object[]) bag;
      int L = array.length;
      // bag has at least 2 elements if it is array
      if (L < 2)
        throw new IllegalArgumentException();
      if (L == 2) {
        if (array[1] == listener)
          bag = array[0];
        else if (array[0] == listener)
          bag = array[1];
      } else {
        int i = L;
        do {
          --i;
          if (array[i] == listener) {
            Object[] tmp = new Object[L - 1];
            System.arraycopy(array, 0, tmp, 0, i);
            System.arraycopy(array, i + 1, tmp, i, L - (i + 1));
            bag = tmp;
            break;
          }
        } while (i != 0);
      }
    }

    return bag;
  }

  /**
   * Check that testClass is accessible from the given loader.
   */
  static boolean testIfCanLoadRhinoClasses(ClassLoader loader) {
    Class<?> testClass = ScriptRuntime.ContextFactoryClass;
    Class<?> x = Kit.classOrNull(loader, testClass.getName());
    if (x != testClass)
      // The check covers the case when x == null =>
      // loader does not know about testClass or the case
      // when x != null && x != testClass =>
      // loader loads a class unrelated to testClass
      return false;
    return true;
  }

  /**
   * If character <tt>c</tt> is a hexadecimal digit, return <tt>accumulator</tt>
   * * 16 plus corresponding number. Otherise return -1.
   */
  public static int xDigitToInt(int c, int accumulator) {
    check : {
      // Use 0..9 < A..Z < a..z
      if (c <= '9') {
        c -= '0';
        if (0 <= c)
          break check;
      } else if (c <= 'F') {
        if ('A' <= c) {
          c -= 'A' - 10;
          break check;
        }
      } else if (c <= 'f')
        if ('a' <= c) {
          c -= 'a' - 10;
          break check;
        }
      return -1;
    }
    return accumulator << 4 | c;
  }
}
